package com.zengjianjun.cloud.disk.platform.baidu;

import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.CharPool;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSON;
import com.zengjianjun.cloud.disk.enums.CloudDiskTypeEnum;
import com.zengjianjun.cloud.disk.exception.CloudDiskException;
import com.zengjianjun.cloud.disk.platform.AbstractCloudDiskClient;
import com.zengjianjun.cloud.disk.platform.baidu.enums.BaiduApiEnum;
import com.zengjianjun.cloud.disk.platform.baidu.enums.BaiduCodeEnum;
import com.zengjianjun.cloud.disk.platform.baidu.result.BaiduBaseResult;
import com.zengjianjun.cloud.disk.platform.baidu.result.GetAccessTokenResult;
import com.zengjianjun.cloud.disk.platform.baidu.result.GetFileInfoResult;
import com.zengjianjun.cloud.disk.platform.baidu.result.GetFileListResult;
import com.zengjianjun.cloud.disk.properties.BaiduProperties;
import com.zengjianjun.cloud.disk.result.AuthInfoResult;
import com.zengjianjun.cloud.disk.result.FileInfoResult;
import com.zengjianjun.cloud.disk.result.FileListResult;
import com.zengjianjun.cloud.disk.result.TokenInfoResult;
import com.zengjianjun.cloud.disk.utils.RemoteUrlUtil;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 百度网盘
 *
 * @author zengjianjun
 */
@Slf4j
@Component
public class BaiduCloudDiskClient extends AbstractCloudDiskClient {

    /**
     * token信息
     */
    private TokenInfoResult tokenInfo;

    private final static Map<String, Object> MAP = new HashMap<>();

    @Resource
    private BaiduProperties baiduProperties;

    @PostConstruct
    public void init() {
        MAP.put("appId", baiduProperties.getAppId());
        MAP.put("appKey", baiduProperties.getAppKey());
        MAP.put("secretkey", baiduProperties.getSecretkey());
        MAP.put("signKey", baiduProperties.getSignKey());
        MAP.put("redirectUri", StringUtils.defaultIfEmpty(baiduProperties.getRedirectUri(), "oob"));
    }

    @Override
    public CloudDiskTypeEnum get() {
        return CloudDiskTypeEnum.Baidu;
    }

    @Override
    public AuthInfoResult getAuthInfo() {
        return AuthInfoResult.builder()
                .cloudDiskTypeEnum(get())
                .authExpired(tokenInfo != null && StringUtils.isNotBlank(tokenInfo.getToken()))
                .expiredTime(tokenInfo != null ? tokenInfo.getExpiredTime() : null)
                .authPageUrl(StrUtil.format(BaiduApiEnum.GET_AUTH_PAGE.getCode(), MAP))
                .build();
    }

    @Override
    public void setTokenInfo(TokenInfoResult tokenInfoResult) {
        this.tokenInfo = tokenInfoResult;
    }

    @Override
    public TokenInfoResult getTokeInfo() {
        return tokenInfo;
    }

    @Override
    public TokenInfoResult refreshAccessToken() {
        if (tokenInfo == null) {
            throw CloudDiskException.build(get(), "token info is null");
        }
        if (StrUtil.isEmpty(tokenInfo.getToken())) {
            throw CloudDiskException.build(get(), "token is null");
        }
        if (StrUtil.isEmpty(tokenInfo.getRefreshToken())) {
            throw CloudDiskException.build(get(), "refreshToken is null");
        }
        MAP.put("refreshToken", tokenInfo.getRefreshToken());
        GetAccessTokenResult result = this.get(BaiduApiEnum.REFRESH_ACCESS_TOKEN, GetAccessTokenResult.class);
        tokenInfo.setToken(result.getAccessToken());
        tokenInfo.setRefreshToken(result.getRefreshToken());
        tokenInfo.setExpiredTime(LocalDateTimeUtil.of(new Date(System.currentTimeMillis() + result.getExpiresIn() * 1000)));
        return tokenInfo;
    }

    @Override
    public List<FileListResult> getFileList(String path) {
        if (StringUtils.isEmpty(path)) {
            path = "/";
        } else {
            try {
                path = URLEncoder.encode(path, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw CloudDiskException.build(get(), "目录url编码失败: {}", path);
            }
        }
        MAP.put("dir", StringUtils.defaultIfEmpty(path, "/"));
        GetFileListResult getFileListResult = this.get(BaiduApiEnum.GET_FILE_LIST, GetFileListResult.class);
        List<GetFileListResult.FileListInfo> resultList = getFileListResult.getList();
        List<FileListResult> list = new ArrayList<>(resultList.size());
        for (GetFileListResult.FileListInfo data : resultList) {
            list.add(FileListResult.builder()
                    .fileId(String.valueOf(data.getFsId()))
                    .path(data.getPath())
                    .fileName(data.getServerFilename())
                    .size(data.getSize())
                    .isdir(data.getIsdir())
                    .category(data.getCategory())
                    .md5(data.getMd5())
                    .dirEmpty(data.getDirEmpty())
                    .build());
        }
        return list;
    }

    @Override
    public FileInfoResult getFileInfo(String fileId) {
        if (StrUtil.isEmpty(fileId)) {
            throw CloudDiskException.build(get(), "文件id不能为空");
        }
        List<String> fsIdList = new ArrayList<>();
        fsIdList.add(fileId);
        List<FileInfoResult> fileInfo = this.getFileInfoList(fsIdList);
        if (fileInfo == null) {
            throw CloudDiskException.build(get(), BaiduCodeEnum.FILE_NO_EXIST.getDesc());
        }
        return fileInfo.get(0);
    }

    @Override
    public List<FileInfoResult> getFileInfoList(List<String> fileIdList) {
        if (CollectionUtils.isEmpty(fileIdList)) {
            throw CloudDiskException.build(get(), "文件id不能为空");
        }
        List<Long> idList = new ArrayList<>(fileIdList.size());
        fileIdList.forEach(data -> idList.add(Long.parseLong(data)));
        String fsids;
        try {
            fsids = URLEncoder.encode(JSON.toJSONString(idList), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw CloudDiskException.build(get(), "文件标识列表url编码失败");
        }
        MAP.put("fsids", fsids);
        GetFileInfoResult getFileInfoResult = this.get(BaiduApiEnum.GET_FILE_INFO, GetFileInfoResult.class);
        List<FileInfoResult> list = new ArrayList<>(getFileInfoResult.getList().size());
        getFileInfoResult.getList().forEach(data -> {
            list.add(FileInfoResult.builder()
                    .isdir(data.getIsdir())
                    .category(data.getCategory())
                    .dLink(this.getFullDLink(data.getDLink()))
                    .filename(data.getFilename())
                    .path(data.getPath())
                    .size(data.getSize())
                    .md5(data.getMd5())
                    .build());
        });
        return list;
    }

    @Override
    public String download(String fileId, String dir) {
        try {
            FileInfoResult fileInfo = this.getFileInfo(fileId);
            String filePath = dir + File.separator + fileInfo.getFilename();
            // 创建文件
            FileUtil.touch(filePath);
            Map<String, String> headMap = new HashMap<>();
            headMap.put("User-Agent", "pan.baidu.com");
            headMap.put("Host", "d.pcs.baidu.com");
            headMap.put("Accept", "*/*");
            // 使用curl下载文件
            List<String> curlCommand = RemoteUrlUtil.getCurlCommand(fileInfo.getDLink(), headMap);
            // 创建ProcessBuilder并设置命令参数
            ProcessBuilder processBuilder = new ProcessBuilder(curlCommand);
            // 启动进程并执行命令
            Process process = processBuilder.start();
            byte[] buffer = new byte[4096];
            @Cleanup OutputStream outputStream = Files.newOutputStream(Paths.get(filePath));
            log.info("文件下载本地路径: {}", filePath);
            int bytesRead;
            AtomicInteger downloadSize = new AtomicInteger();
            while ((bytesRead = process.getInputStream().read(buffer)) != -1) {
                downloadSize.addAndGet(bytesRead);
                // 写入响应输出流
                outputStream.write(buffer, 0, bytesRead);
                String progress = super.progress(downloadSize.get(), fileInfo.getSize());
                log.info("文件下载进度：fileId = {}, fileName = {}, progress = {}", fileId, fileInfo.getFilename(), progress);
            }
            // 等待 Curl 进程完成
            int code = process.waitFor();
            log.info("command execute code = {}", code);
            StringBuilder errorMsg = new StringBuilder();
            InputStream errorStream = process.getErrorStream();
            while ((bytesRead = errorStream.read(buffer)) != -1) {
                errorMsg.append(new String(buffer, 0, bytesRead, StandardCharsets.UTF_8));
            }
            log.info("command execute info: {}", errorMsg);
            return filePath;
        } catch (Exception e) {
            log.error("下载文件到本地失败：fsId = {}", fileId, e);
            throw CloudDiskException.build(get(), "下载文件到本地失败: {}", e.getMessage());
        }
    }

    /**
     * 获取完整下载链接
     *
     * @param dLink 下载链接
     * @return 完整下载链接
     */
    private String getFullDLink(String dLink) {
        TokenInfoResult tokeInfo = this.getTokeInfo();
        return dLink + CharPool.AMP + "access_token=" + tokeInfo.getToken();
    }

    /**
     * 百度云api get请求
     *
     * @param baiduApiEnum 请求接口
     * @param clazz        响应转换对象
     * @param <T>          T
     * @return 响应结果
     */
    private <T extends BaiduBaseResult> T get(BaiduApiEnum baiduApiEnum, Class<T> clazz) {
        if (baiduApiEnum != BaiduApiEnum.GET_ACCESS_TOKEN && baiduApiEnum != BaiduApiEnum.REFRESH_ACCESS_TOKEN) {
            if (tokenInfo == null || StringUtils.isEmpty(tokenInfo.getToken())) {
                throw CloudDiskException.build(get(), "token过期");
            }
            MAP.put("accessToken", tokenInfo.getToken());
        }
        String url = StrUtil.format(baiduApiEnum.getCode(), MAP);
        log.info("[{}] start: url = {}", baiduApiEnum.getDesc(), url);
        long startTime = System.currentTimeMillis();
        @Cleanup HttpResponse execute = HttpRequest.get(url).timeout(3000).execute();
        String result = execute.body();
        long endTime = System.currentTimeMillis();
        log.info("[{}] end: result = {}, 耗时 = {}ms", baiduApiEnum.getDesc(), result, endTime - startTime);
        T t = JSON.parseObject(result, clazz);
        this.checkToken(t);
        if (!BaiduCodeEnum.isSuccess(t)) {
            throw CloudDiskException.build(get(), BaiduCodeEnum.getErrorMsg(t));
        }
        return t;
    }

    /**
     * 校验token信息
     *
     * @param baiduBaseResult 百度api基础响应
     */
    private void checkToken(BaiduBaseResult baiduBaseResult) {
        if (BaiduCodeEnum.isTokenError(baiduBaseResult)) {
            try {
                // 刷新token
                this.refreshAccessToken();
            } catch (Exception e) {
                this.tokenInfo = null;
                // 刷新失败
                log.info("刷新accessToken失败：", e);
            }
        }
    }
}
